Jestの基本的な書き進め方

Jestの基本的な書き進め方

Jestの書き進め方をアウトプットしてみました。テストコードの書き進め方やテストスイート、マッチャーについて書いています。
Clock Icon2024.12.20

はじめに

こんにちは、アノテーションのなかたです。
今回は、Jestでテストについて学んでいるため、Jestにおけるテストコードの書き進め方をアウトプットしてみました。

このブログで書いていること

  • テストコードの書き進め方
  • テストスイート、マッチャー

このブログで書いていないこと

  • セットアップやテストコマンドの実行方法
  • モック
  • テストに対する考え方

これらは後々、別のブログとして書こうと思います。

テスト対象とするソースコード

まず、テストする対象として銀行のアカウントを表す BankAccount クラスを定義します。
ソースコードは、各セクションでも書いているので、こちらを毎回参照する必要はありません。

BankAccount
bank.ts
export class BankAccount {
  private balance: number; // 残高
  private transactions: Array<{ type: string; amount: number; date: Date }>; // 取引履歴

  public constructor(
    public accountHolder: string,
    initialBalance = 0,
  ) {
    if (initialBalance < 0) {
      throw new Error('Initial balance cannot be negative');
    }
    this.balance = initialBalance;
    this.transactions = [];
  }

  // 入金処理
  public deposit(amount: number): number {
    if (amount <= 0) {
      throw new Error('Deposit amount must be positive');
    }
    this.balance += amount;
    this.transactions.push({ type: 'deposit', amount, date: new Date() });
    return this.balance;
  }

  // 出金処理
  public withdraw(amount: number): number {
    if (amount <= 0) {
      throw new Error('Withdrawal amount must be positive');
    }
    if (amount > this.balance) {
      throw new Error('Insufficient funds');
    }
    this.balance -= amount;
    this.transactions.push({ type: 'withdraw', amount, date: new Date() });
    return this.balance;
  }

  private printGetBalance(): void {
    console.log('getBalance called');
  }

  // 残高を取得
  public getBalance(): number {
    this.printGetBalance();
    return this.balance;
  }
}

簡単なテストコードを書いてみる

BankAccount をテストするにはインスタンスを作成する必要がありそうです。
以下のように書いてみました。

bank.test.ts
import { BankAccount } from '#/bank';

const bankAccount = new BankAccount('user1', 1000);

こちらで実行してみると、エラーが発生します。

 FAIL  __tests__/bank.test.ts
  ● Test suite failed to run

    Your test suite must contain at least one test.

      at onResult (node_modules/@jest/core/build/TestScheduler.js:133:18)
      at node_modules/@jest/core/build/TestScheduler.js:254:19
      at node_modules/emittery/index.js:363:13
          at Array.map (<anonymous>)
      at Emittery.emit (node_modules/emittery/index.js:361:23)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        9.307 s
Ran all test suites matching /\/home\/user\/dev\/__tests__\/bank\.test\.ts/i with tests matching "BankAccount Test".

Your test suite must contain at least one test.とありますので、少なくとも1つのテストコードを書く必要がありそうです。

test(もしくはit)というテストスイートに入れてあげることでテストが通る様になります。

import { BankAccount } from '#/bank';

test('BankAccount', () => {
  const bankAccount = new BankAccount('user1', 1000);
});

// テスト結果
//  PASS  __tests__/bank.test.ts (6.626 s)
//   ✓ BankAccount (3 ms)

// Test Suites: 1 passed, 1 total
// Tests:       1 passed, 1 total
// Snapshots:   0 total
// Time:        7.198 s, estimated 9 s
// Ran all test suites matching /\/home\/user\/dev\/__tests__\/bank\.test\.ts/i with tests matching "bankAccount$".

expect + toBe による単純なテスト

では、簡単なテストを書いてみます。
getBalance 関数が簡単にテストできそうなので、こちらをテスト対象とします。

// ソースコード
// export class BankAccount {
//   private balance: number; // 残高
//   private transactions: Array<{ type: string; amount: number; date: Date }>; // 取引履歴

//   public constructor(
//     public accountHolder: string,
//     initialBalance = 0,
//   ) {
//     if (initialBalance < 0) {
//       throw new Error('Initial balance cannot be negative');
//     }
//     this.balance = initialBalance;
//     this.transactions = [];
//   }
// }

import { BankAccount } from '#/bank';

test('BankAccount', () => {
  const bankAccount = new BankAccount('user1', 1000);
  expect(bankAccount.getBalance()).toBe(1000);
});

// テスト結果
//  PASS  __tests__/bank.test.ts (9.702 s)
//   ✓ BankAccount (5 ms)

// Test Suites: 1 passed, 1 total
// Tests:       1 passed, 1 total
// Snapshots:   0 total
// Time:        10.291 s
// Ran all test suites matching /\/home\/user\/dev\/__tests__\/bank\.test\.ts/i with tests matching "bankAccount$"

expecttoBe による組み合わせでテストできます。
toBe は Jest における最も単純なマッチャーで、expect に渡した値と照合するだけです。

Jestのマッチャー

Jest はマッチャーが豊富であるため、値によって適切なマッチャーを選ぶとシンプルで適切なテストコードが書けます。

マッチャー 説明
toBe(value) 厳密な等価性をテストします(===)。 expect(2 + 2).toBe(4);
toEqual(value) オブジェクトや配列の内容が等しいかをテストします。 expect({a: 1}).toEqual({a: 1});
toBeTruthy() 値がtrueと評価されるかをテストします。 expect(1).toBeTruthy();
toContain(item) 配列や文字列が特定の要素を含むかをテストします。 expect([1, 2, 3]).toContain(2);
toThrow() 関数がエラーをスローするかをテストします。 expect(() => { throw new Error(); }).toThrow();
など

https://jestjs.io/ja/docs/using-matchers

toThrowによる例外テスト

次に、コンストラクタでは例外が発生することをテストします。
toThrowにエラーやエラーメッセージを渡すことでテストが可能です。
また、expectには() =>などを用いて、関数を渡す必要があります。

// ソースコード
// export class BankAccount {
//   private balance: number; // 残高
//   private transactions: Array<{ type: string; amount: number; date: Date }>; // 取引履歴

//   public constructor(
//     public accountHolder: string,
//     initialBalance = 0,
//   ) {
//     if (initialBalance < 0) {
//       throw new Error('Initial balance cannot be negative');
//     }
//     this.balance = initialBalance;
//     this.transactions = [];
//   }
// }

import { BankAccount } from '#/bank';

test('BankAccount initialBalance greater than 0', () => {
  expect(() => new BankAccount('user1', -1)).toThrow('Initial balance cannot be negative');
  // expect(() => new BankAccount('user1', -1)).toThrow(Error);  # Errorでも可
});

describetest をまとめる

BankAccount に対するテストが増えてしまったため、describeでまとめてみます。


import { BankAccount } from '#/bank';

// // Before: BankAccountに対するテスト群
// test('BankAccount getBalance', () => {
//   const bankAccount = new BankAccount('user1', 1000);
//   expect(bankAccount.getBalance()).toBe(1000);
// });

// test('BankAccount initialBalance greater than 0', () => {
//   expect(() => new BankAccount('user1', -1)).toThrow('Initial balance cannot be negative');
// });

describe('BankAccount', () => {
  test('getBalance', () => {
    const bankAccount = new BankAccount('user1', 1000);
    expect(bankAccount.getBalance()).toBe(1000);
  });

  test('initialBalance greater than 0', () => {
    expect(() => new BankAccount('user1', -1)).toThrow('Initial balance cannot be negative');
  });
});

describe スコープ内でオブジェクトを使い回す

また、describeはスコープを共有するため、オブジェクトを使い回せます。
試しに コンストラクタに渡す accountHolder を使い回してみます。

import { BankAccount } from '#/bank';

describe('BankAccount', () => {
  const accountHolder = 'user1'; // describeスコープに変数を宣言

  test('getBalance', () => {
    const bankAccount = new BankAccount(accountHolder, 1000);
    expect(bankAccount.getBalance()).toBe(1000);
  });

  test('initialBalance greater than 0', () => {
    expect(() => new BankAccount(accountHolder, -1)).toThrow('Initial balance cannot be negative');
  });
});

モックデータが重複してしまい、コードが長くなってしまう際に便利です。
しかし、他テストによりオブジェクトの状態が変化してしまう可能性を考慮する必要があります。

参考

https://jestjs.io/ja/

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.